package calendar;

import edu.gmu.client.FrontEnd;
import edu.gmu.messages.EndPoint;
import java.io.IOException;
import java.io.Serializable;
import java.util.Date;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;

/* this class represents a single user's calendar */
import java.util.logging.Level;
import java.util.logging.Logger;
public class CalendarObj implements CalendarObjInt, Serializable
{
  private static final long serialVersionUID = 23048923L;

  User user;
  ArrayList events;
  transient CalendarMgr manager;
  transient volatile boolean connected;
  ArrayList members;
  transient CallbackInt client;
  transient CalendarStub replica;
  transient volatile boolean locked;

  public CalendarObj( )
  {
    super();
    this.manager = null;
    this.connected = false;
    this.members = new ArrayList();
    this.client = null;
    this.replica = null;
    this.locked = false;
  }
  
  public CalendarObj( User user, CalendarMgr manager )
  {
    super();
    this.manager = manager;
    this.client = null;
    this.replica = null;
    this.user = user;
    events = new ArrayList();
    connected = false;
    members = new ArrayList();
    locked = false;
  }

  public void setManager( CalendarMgr manager )
  {
    this.manager = manager;
  }

  public void setCallback( CallbackInt stub )
  {
    this.client = stub;
  }

  public boolean connectReplica( EndPoint replicaEndpoint )
  {
    if ( this.replica == null && this.manager.getType().getId() == Type.PRIMARY.getId() )
    {
      try
      {
        replica = new CalendarStub( replicaEndpoint );
      }
      catch (Exception e)
      {
        return false;
      }
    }
    return true;
  }
  
  public void disconnectReplica( )
  {
    if( this.replica != null )
    {
        this.replica.stop();
    }
  }

  /* retrieve a list of events based on user and start/end dates */
  public synchronized ArrayList retrieveEvents( User user, Date start,
    Date end ) throws IOException
  {
    ArrayList eventList = new ArrayList();
    ArrayList newEventList = new ArrayList();

    if ( this.user.equals( user ) )
    {
      eventList = events;
      newEventList.addAll( 0, eventList );
    }
    else
    {
        FrontEnd frontEnd = new FrontEnd( );
        ArrayList othersEvents = frontEnd.retreiveEvents(user, start, end);
        if( othersEvents != null )
        {
            newEventList.addAll( 0, othersEvents );
        }
    }

    if ( start == null && end == null )
    {
      if ( newEventList.isEmpty() ) return null;
      else return newEventList;
    }

    Iterator itr = eventList.iterator();
    while ( itr.hasNext() )
    {
      Event e = (Event)itr.next();

      /* only return events within provided range */
      if ( (e.getStart().compareTo(start) > 0 ||
            e.getStart().compareTo(start) == 0) &&
           (e.getEnd().compareTo(end) < 0 ||
            e.getEnd().compareTo(end) == 0) ) {}
      else newEventList.remove( e );
    }

    if ( newEventList.isEmpty() ) return null;
    else return newEventList;
  }

  public boolean lock()
  {
    if ( !locked )
    {
      locked = true;
      return true;
    }
    else return false;
  }

  public void unlock()
  {
    locked = false;
  }

  /* schedule a new event, modify an event, or delete an event */
  public ArrayList scheduleEvent(ArrayList users, Event newEvent,
    Event oldEvent, boolean addToThis ) throws IOException
  {
    Type t = this.manager.getType();

    // replica has to be able to find other calendar replicas (replicaStub)
    if ( this.replica != null && t == Type.PRIMARY )
    {
       if ( replica.scheduleEvent(users, newEvent, oldEvent, addToThis) == null )
       {
           return null;
       }
    }
    
    if ( addToThis ) /* add event to this calendar */
    {
      if ( oldEvent != null )
      {
        ArrayList eventsCopy = new ArrayList();
        eventsCopy.addAll( 0, events );

        Iterator itr = eventsCopy.iterator();
        while ( itr.hasNext() )
        {
          Event e = (Event)itr.next();
          if ( e.getId() == oldEvent.getId() ) events.remove( e );
        }
      }

      /* check to ensure this calendar can accommodate this event */
      if ( checkSchedule( this.user, newEvent ) )
      {
        events.add( newEvent );
        members.add( this.user );
        newEvent.addUser( this.user );

//        if (    client != null &&
//                t == Type.PRIMARY &&
//                newEvent.getType() == 3 )
//        {
//          client.notifyClientEvent( newEvent, false );
//        }
      }
    }

    if ( newEvent == null && oldEvent == null )
    {
      System.out.println( "Error: Null passed in as event arguments to " +
        "CalendarObj: scheduleEvent" );
      manager.writeToDisk();
      return null;
    }

    /* attempt to add the event to the other users calendars (recursion) */
    if ( ((newEvent != null && newEvent.getType() == 3) ||
          (oldEvent != null && oldEvent.getType() == 3)) && !addToThis )
    {
      addToThis = true;
      FrontEnd frontEnd = new FrontEnd( );
      
      // lock calendars
      Iterator itr = users.iterator();
      while ( itr.hasNext() )
      {
          
        User u = (User)itr.next();
        CalendarStub stub = frontEnd.getStub( u, t );
        if ( !stub.lock() ) return null;
      }
      
      if ( newEvent.getDesc().equals( "sleep" ) )
      {
          try
            {
                Thread.sleep(10000);
            } catch (InterruptedException ex)
            {
                System.out.println( "Thread cannot sleep." );
            }
      }

      // schedule event
      itr = users.iterator();
      while ( itr.hasNext() )
      {
        User u = (User)itr.next();
        CalendarStub stub = frontEnd.getStub( u, t );
        if ( !u.getName().equals(this.user.getName() ) )
          members.addAll( stub.scheduleEvent( users, newEvent, oldEvent, addToThis ) );
        else stub.scheduleEvent( users, newEvent, oldEvent, addToThis );
      }

      // update event user list (only for new and edit)
      if ( newEvent != null )
      {
        itr=users.iterator();
        while ( itr.hasNext() )
        {
          User u = (User)itr.next();
          CalendarStub stub = frontEnd.getStub( u, t );
          stub.updateEventUsers( members, newEvent );
        }
      }
      
      ArrayList temp = new ArrayList( members );

      // clear all member lists
      itr = users.iterator();
      while ( itr.hasNext() )
      {
        User u = (User)itr.next();
        CalendarStub stub = frontEnd.getStub( u, t );
        stub.clearMembers();
      }

      members = temp;

      // unlock calendars
      itr = users.iterator();
      while ( itr.hasNext() )
      {
        User u = (User)itr.next();
        CalendarStub stub = frontEnd.getStub( u, t );
        stub.unlock();
      }
    }
    manager.writeToDisk();
    return members;
  }

  public void updateEventUsers ( ArrayList users, Event event )
  {
    boolean foundUser = true;
    Event tempEvent = new Event();
    User tempUser = new User();
    if ( !users.isEmpty() && !users.contains( event.getOwner() )  )
    {
      Collections.sort( users );
      tempUser = (User)users.get(0);
      foundUser = false;
    }
    users.remove( this.user );
    Iterator itr = events.iterator();
    while ( itr.hasNext() )
    {
      Event e = (Event)itr.next();
      if ( e.getId() == event.getId() )
      {
        tempEvent = e;
        Iterator itr2 = users.iterator();
        while ( itr2.hasNext() )
        {
          User u = (User)itr2.next();
          e.addUser( u );
        }
      }
    }
    if ( !foundUser ) tempEvent.setOwner( tempUser );
  }

  /* clear the temporary members list shared by all calendars */
  public synchronized void clearMembers() throws IOException
  {
    members.clear();
    manager.writeToDisk();
    if ( this.replica != null && this.manager.getType() == Type.PRIMARY  )
    {
        replica.clearMembers();
    }
  }

  // register a user interface client with this calendar for callbacks
  public synchronized void registerCallback(CallbackInt client) throws IOException
  {
    if ( this.replica != null ) replica.registerCallback(client);
    this.client = client;
  }

  // unregister a user interface client from this calendar
  public synchronized void unregisterCallback() throws IOException
  {
    if ( this.replica != null ) replica.unregisterCallback();
    this.client = null;
  }

  public User getUser()
  {
    return this.user;
  }

  public synchronized ArrayList getEvents()
  {
    return this.events;
  }

  public synchronized void setIsConnected ( boolean connected )
  {
    this.connected = connected;
    manager.writeToDisk();
  }

  public synchronized boolean isConnected () throws IOException
  {
    return connected;
  }

  /* notify a client that an event is about to occur */
  public synchronized void notifyClientEvent( Event event )
  {
    if ( client != null && this.manager.getType() == Type.PRIMARY )
    {
      try
      {
        client.notifyClientEvent( event, true );
      }
      catch(IOException ex)
      {
        System.out.println( "CalendarObj: notifyClientEvent -> " + ex );
      }
    }
    event.setIsSent();
    manager.writeToDisk(); /* TODO: replicate this call? */
  }

  /* notify a client that a new calendar has been created */
  public synchronized void notifyClientUser( User user, ArrayList users )
  {
    if ( client != null && this.manager.getType() == Type.PRIMARY )
    {
        try
        {
          client.notifyClientUser( user, users );
        }
        catch(IOException ex)
        {
          System.out.println( "CalendarObj: notifyClientUser -> " + ex );
        }
    }
  }

  public synchronized void notifyReconnect()
  {
    if ( client != null && this.manager.getType() == Type.BACKUP )
    {
        try
        {
          client.reconnect();
        }
        catch(IOException ex)
        {
          System.out.println( "CalendarObj: notifyClientUser -> " + ex );
        }
    }
  }

  /* check the current calendar's schedule for availability */
  private synchronized boolean checkSchedule( User user, Event event )
  {
    if ( user == null )
    {
      return false;
    }

    if ( event == null )
    {
      return false;
    }

    ArrayList eventsToCheck = new ArrayList();

    /* if the user to check owns this calendar */
    if ( this.user.getId() == user.getId() )
    {
      eventsToCheck = this.events;
    }
    else
    {
        eventsToCheck = manager.getCalendar( user ).getEvents();
    }

    if ( eventsToCheck != null )
    {
      Iterator itr = eventsToCheck.iterator();
      while ( itr.hasNext() )
      {
        Event e = (Event)itr.next();

        /* if the new event starts or ends within this event */
        if (((event.getStart().compareTo( e.getStart() ) > 0) &&
             (event.getStart().compareTo( e.getEnd() ) < 0) ) ||
             ((event.getEnd().compareTo( e.getStart() ) > 0) &&
             (event.getEnd().compareTo( e.getEnd() ) < 0) ) ||
             ((event.getStart().compareTo( e.getStart() ) < 0) &&
             (event.getEnd().compareTo( e.getEnd() ) > 0)) ||
             event.getStart().compareTo( e.getStart() ) == 0 ||
             event.getEnd().compareTo( e.getEnd() ) == 0 )
        {
          return false;
        }
      }
    }
    else System.out.println( "Null eventsToCheck for: " + this.user.getName() );
    return true;
  }
}
